import { isNumber } from 'lodash'

export interface Position {
  w: number,
  h: number,
  x: number,
  y: number,
}

export interface LayoutItemRequired extends Position {
  id: number | string
}

export interface LayoutItem extends LayoutItemRequired {
  minW?: number,
  minH?: number,
  maxW?: number,
  maxH?: number,
  moved?: boolean,
  static?: boolean,
  isDraggable?: boolean,
  isResizable?: boolean
}

export type RawLayoutItem = Record<keyof LayoutItem, unknown>
export type Layout = LayoutItem[];
export type RawLayout = Array<RawLayoutItem>;

/**
 * Return the bottom coordinate of the layout.
 * @param  {Array} layout Layout array.
 * @return {Number}       Bottom coordinate.
 */
export function bottom (layout: Layout): number {
  let max = 0
  let bottomY
  for (let index = 0, len = layout.length; index < len; index++) {
    bottomY = layout[index].y + layout[index].h
    if (bottomY > max) max = bottomY
  }
  return max
}

export function cloneLayout (layout: Layout): Layout {
  const newLayout = Array(layout.length)
  for (let index = 0, len = layout.length; index < len; index++) {
    newLayout[index] = cloneLayoutItem(layout[index])
  }
  return newLayout
}

// Fast path to cloning, since this is monomorphic
export function cloneLayoutItem (layoutItem: LayoutItem): LayoutItem {
  /* return {
    w: layoutItem.w, h: layoutItem.h, x: layoutItem.x, y: layoutItem.y, id: layoutItem.id,
    minW: layoutItem.minW, maxW: layoutItem.maxW, minH: layoutItem.minH, maxH: layoutItem.maxH,
    moved: Boolean(layoutItem.moved), static: Boolean(layoutItem.static),
    // These can be null
    isDraggable: layoutItem.isDraggable, isResizable: layoutItem.isResizable
  }; */
  return JSON.parse(JSON.stringify(layoutItem))
}

/**
 * Given two layout items, check if they collide.
 * @return {Boolean}   True if colliding.
 */
export function collides (l1: Position, l2: Position): boolean {
  if (l1 === l2) return false // same element
  if (l1.x + l1.w <= l2.x) return false // l1 is left of l2
  if (l1.x >= l2.x + l2.w) return false // l1 is right of l2
  if (l1.y + l1.h <= l2.y) return false // l1 is above l2
  if (l1.y >= l2.y + l2.h) return false // l1 is below l2
  return true // boxes overlap
}

/**
 * Given a layout, compact it.
 * This involves going down each y coordinate and removing gaps between items.
 * @param  {Array} layout Layout.
 * @param  {Boolean} verticalCompact Whether or not to compact the layout
 *   vertically.
 * @return {Array}       Compacted Layout.
 */
export function compact (layout: Layout, verticalCompact: boolean): Layout {
  // Statics go in the compareWith array right away so items flow around them.
  const compareWith = getStatics(layout)
  // We go through the items by row and column.
  const sortLayoutItems = sortLayoutItemsByRowCol(layout)
  // Holding for new items.
  const outLayoutItems = Array(layout.length)

  for (let index = 0, len = sortLayoutItems.length; index < len; index++) {
    let layoutItem = sortLayoutItems[index]

    // Don't move static elements
    if (!layoutItem.static) {
      layoutItem = compactItem(compareWith, layoutItem, verticalCompact)

      // Add to comparison array. We only collide with items before this one.
      // Statics are already in this array.
      compareWith.push(layoutItem)
    }

    // Add to output array to make sure they still come out in the right order.
    outLayoutItems[layout.indexOf(layoutItem)] = layoutItem

    // Clear moved flag, if it exists.
    layoutItem.moved = false
  }
  return outLayoutItems
}

/**
 * Compact an item in the layout.
 */
export function compactItem (compareWith: Layout, layoutItem: LayoutItem, verticalCompact: boolean): LayoutItem {
  if (verticalCompact) {
    // Move the element up as far as it can go without colliding.
    while (layoutItem.y > 0 && !getFirstCollision(compareWith, layoutItem)) {
      layoutItem.y--
    }
  }

  // Move it down, and keep moving it down if it's colliding.
  let collides
  while ((collides = getFirstCollision(compareWith, layoutItem))) {
    layoutItem.y = collides.y + collides.h
  }
  return layoutItem
}

/**
 * Given a layout, make sure all elements fit within its bounds.
 *
 * @param  {Array} layout Layout array.
 * @param  {Number} bounds Number of columns.
 */
export function correctBounds (layout: Layout, bounds: { cols: number }): Layout {
  const collidesWith = getStatics(layout)
  for (let index = 0, len = layout.length; index < len; index++) {
    const l = layout[index]
    // Overflows right
    if (l.x + l.w > bounds.cols) l.x = bounds.cols - l.w
    // Overflows left
    if (l.x < 0) {
      l.x = 0
      l.w = bounds.cols
    }
    if (!l.static) {
      collidesWith.push(l)
    } else {
      // If this is static and collides with other statics, we must move it down.
      // We have to do something nicer than just letting them overlap.
      while (getFirstCollision(collidesWith, l)) {
        l.y++
      }
    }
  }
  return layout
}

/**
 * Get a layout item by ID. Used so we can override later on if necessary.
 *
 * @param  {Array}  layout Layout array.
 * @param  {String} id     ID
 * @return {LayoutItem}    Item at ID.
 */
export function getLayoutItem (layout: Layout, id: string): LayoutItem | undefined {
  for (let index = 0, len = layout.length; index < len; index++) {
    if (layout[index].id === id) return layout[index]
  }
}

/**
 * Returns the first item this layout collides with.
 * It doesn't appear to matter which order we approach this from, although
 * perhaps that is the wrong thing to do.
 * @param  {Layout} layout Layout
 * @param  {Object} layoutItem Layout item.
 * @return {Object|undefined}  A colliding layout item, or undefined.
 */
export function getFirstCollision (layout: Layout, layoutItem: LayoutItem): LayoutItem | undefined {
  for (let index = 0, len = layout.length; index < len; index++) {
    if (collides(layout[index], layoutItem)) return layout[index]
  }
}

export function getAllCollisions (layout: Layout, layoutItem: Position): Array<LayoutItem> {
  return layout.filter((l) => collides(l, layoutItem))
}

/**
 * Get all static elements.
 * @param  {Array} layout Array of layout objects.
 * @return {Array}        Array of static layout items..
 */
export function getStatics (layout: Layout): Array<LayoutItem> {
  return layout.filter((l) => l.static)
}

/**
 * Move an element. Responsible for doing cascading movements of other elements.
 * @param  {Array}      layout Full layout to modify.
 * @param  {LayoutItem} layoutItem      element to move.
 * @param  {Number}     [targetPos]     X/Y position in grid units.
 * @param  {Object}     [options]       options
 * @param  {boolean}    [options.isUserAction]     If true, designates that the item we're moving is
 *                                                  being dragged/resized by th user.
 * @param  {boolean}    [options.preventCollision]  Prevent collision
 * @param  {function}   [moveEventHandler]          Move process event
 */
export function moveLayoutItem (
  layout: Layout,
  layoutItem: LayoutItem,
  targetPos: {
    x: number | undefined,
    y: number | undefined,
  },
  options: {
    isUserAction?: boolean,
    preventCollision?: boolean
  },
  moveEventHandler?: (event: {
    type: string,
    collisions?: LayoutItem[],
  }) => void
): Layout {
  if (layoutItem.static) return layout

  const isUserAction = options.isUserAction || false
  const preventCollision = options.preventCollision || false

  // Short-circuit if nothing to do.
  // if (layoutItem.y === y && layoutItem.x === x) return layout;

  const oldX = layoutItem.x
  const oldY = layoutItem.y

  // same w/h swap
  const swapItem = layout.find((e: LayoutItem) => {
    let canSwap = true

    if (e === layoutItem ||
      e.w !== layoutItem.w ||
      e.h !== layoutItem.h ||
      !isNumber(targetPos.y) ||
      !isNumber(targetPos.x) ||
      e.y !== targetPos.y) {
      return false
    }

    if (e.x <= targetPos.x &&
      Math.abs(e.x + e.w - targetPos.x) > 1) {
      canSwap = false
    }

    if (e.x > targetPos.x &&
      Math.abs(targetPos.x + layoutItem.w - e.x) > 1) {
      canSwap = false
    }

    return canSwap
  })
  if (swapItem) {
    // swapItem.x = oldX;
    // swapItem.y = oldY;
    // console.log(swapItem?.id);
    // return layout;
  }

  const movingUp = targetPos.y && layoutItem.y > targetPos.y
  // This is quite a bit faster than extending the object
  if (typeof targetPos.x === 'number') layoutItem.x = targetPos.x
  if (typeof targetPos.y === 'number') layoutItem.y = targetPos.y
  layoutItem.moved = true

  // If this collides with anything, move it.
  // When doing this comparison, we have to sort the items we compare with
  // to ensure, in the case of multiple collisions, that we're getting the
  // nearest collision.
  let sortLayoutItems = sortLayoutItemsByRowCol(layout)
  if (movingUp) sortLayoutItems = sortLayoutItems.reverse()
  const collisions = getAllCollisions(sortLayoutItems, layoutItem)

  // [X] move processing event
  if (collisions.length) {
    moveEventHandler && moveEventHandler({
      type: 'collide',
      collisions: collisions
    })
  }

  if ((preventCollision && collisions.length)) {
    layoutItem.x = oldX
    layoutItem.y = oldY
    layoutItem.moved = false
    return layout
  }

  // Move each item that collides away from this element.
  for (let index = 0, len = collisions.length; index < len; index++) {
    const collision = collisions[index]

    // console.log('resolving collision between', layoutItem.id, 'at', layoutItem.y, 'and', collision.id, 'at', collision.y);

    // Short circuit so we can't infinite loop
    if (collision.moved) continue

    // This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
    if (layoutItem.y > collision.y && layoutItem.y - collision.y > collision.h / 4) continue

    // Don't move static items - we have to move *this* element away
    if (collision.static) {
      layout = moveLayoutItemAwayFromCollision(layout, collision, layoutItem, isUserAction)
    } else {
      layout = moveLayoutItemAwayFromCollision(layout, layoutItem, collision, isUserAction)
    }
  }

  moveEventHandler && moveEventHandler({
    type: 'move'
  })

  return layout
}

/**
 * This is where the magic needs to happen - given a collision, move an element away from the collision.
 * We attempt to move it up if there's room, otherwise it goes below.
 *
 * @param  {Array} layout            Full layout to modify.
 * @param  {LayoutItem} collidesWith Layout item we're colliding with.
 * @param  {LayoutItem} itemToMove   Layout item we're moving.
 * @param  {Boolean} [isUserAction]  If true, designates that the item we're moving is being dragged/resized
 *                                   by the user.
 */
export function moveLayoutItemAwayFromCollision (
  layout: Layout,
  collidesWith: LayoutItem,
  itemToMove: LayoutItem,
  isUserAction: boolean
): Layout {
  // we're already colliding
  const preventCollision = false
  // If there is enough space above the collision to put this element, move it there.
  // We only do this on the main collision as this can get funky in cascades and cause
  // unwanted swapping behavior.
  if (isUserAction) {
    // Make a mock item so we don't modify the item here, only modify in moveLayoutItem.
    const fakeItem: LayoutItem = {
      x: itemToMove.x,
      y: itemToMove.y,
      w: itemToMove.w,
      h: itemToMove.h,
      id: '-1'
    }
    fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0)
    if (!getFirstCollision(layout, fakeItem)) {
      return moveLayoutItem(layout, itemToMove, {
        x: undefined,
        y: fakeItem.y
      }, {
        preventCollision
      })
    }
  }

  // Previously this was optimized to move below the collision directly, but this can cause problems
  // with cascading moves, as an item may actually leapfrog a collision and cause a reversal in order.
  return moveLayoutItem(layout, itemToMove, {
    x: undefined,
    y: itemToMove.y + 1
  }, {
    preventCollision
  })
}

export interface TransformItemStyle {
  transform: string,
  WebkitTransform: string,
  MozTransform: string,
  msTransform: string,
  OTransform: string,
  width: string,
  height: string,
  position: string
}

export function setTransform (top: number, left: number, width: number, height: number): TransformItemStyle {
  // Replace unit less items with px
  const translate = 'translate3d(' + left + 'px,' + top + 'px, 0)'
  return {
    transform: translate,
    WebkitTransform: translate,
    MozTransform: translate,
    msTransform: translate,
    OTransform: translate,
    width: width + 'px',
    height: height + 'px',
    position: 'absolute'
  }
}

/**
 * Just like the setTransform method, but instead it will return a negative value of right.
 *
 * @param top
 * @param right
 * @param width
 * @param height
 * @returns {{transform: string, WebkitTransform: string, MozTransform: string, msTransform: string, OTransform: string, width: string, height: string, position: string}}
 */
export function setTransformRtl (top: number, right: number, width: number, height: number): TransformItemStyle {
  // Replace unit less items with px
  const translate = 'translate3d(' + right * -1 + 'px,' + top + 'px, 0)'
  return {
    transform: translate,
    WebkitTransform: translate,
    MozTransform: translate,
    msTransform: translate,
    OTransform: translate,
    width: width + 'px',
    height: height + 'px',
    position: 'absolute'
  }
}

export interface LayoutPositionStyle {
  top: string,
  right?: string,
  left?: string,
  width: string,
  height: string,
  position: string,
}

export function setTopLeft (top: number, left: number, width: number, height: number): LayoutPositionStyle {
  return {
    top: top + 'px',
    left: left + 'px',
    width: width + 'px',
    height: height + 'px',
    position: 'absolute'
  }
}

/**
 * Just like the setTopLeft method, but instead, it will return a right property instead of left.
 *
 * @param top
 * @param right
 * @param width
 * @param height
 * @returns {{top: string, right: string, width: string, height: string, position: string}}
 */
export function setTopRight (top: number, right: number, width: number, height: number): LayoutPositionStyle {
  return {
    top: top + 'px',
    right: right + 'px',
    width: width + 'px',
    height: height + 'px',
    position: 'absolute'
  }
}

/**
 * Get layout items sorted from top left to right and down.
 *
 * @return {Array} Array of layout objects.
 * @return {Array}        Layout, sorted static items first.
 */
export function sortLayoutItemsByRowCol (layout: Layout): Layout {
  return [...layout].sort(function (a, b) {
    if (a.y === b.y && a.x === b.x) {
      return 0
    }
    if (a.y > b.y || (a.y === b.y && a.x > b.x)) {
      return 1
    }
    return -1
  })
}

/**
 * Validate a layout. Throws errors.
 *
 * @param  {Array}  layout        Array of layout items.
 * @param  {String} [contextName] Context name for errors.
 * @throw  {Error}                Validation error.
 */
export function validateLayout (layout: RawLayout, contextName: string): void {
  contextName = contextName || 'Layout'
  const subProps = ['x', 'y', 'w', 'h']
  const keyArr = []
  if (!Array.isArray(layout)) throw new Error(contextName + ' must be an array!')
  for (let index = 0, len = layout.length; index < len; index++) {
    const item = layout[index]
    for (let j = 0; j < subProps.length; j++) {
      if (typeof item[subProps[j] as 'x' | 'y' | 'w' | 'h'] !== 'number') {
        throw new Error('VueGridLayout: ' + contextName + '[' + index + '].' + subProps[j] + ' must be a number!')
      }
    }

    if (item.id === undefined || item.id === null) {
      throw new Error('VueGridLayout: ' + contextName + '[' + index + '].id cannot be null!')
    }

    if (typeof item.id !== 'number' && typeof item.id !== 'string') {
      throw new Error('VueGridLayout: ' + contextName + '[' + index + '].id must be a string or number!')
    }

    if (keyArr.indexOf(item.id) >= 0) {
      throw new Error('VueGridLayout: ' + contextName + '[' + index + '].id must be unique!')
    }
    keyArr.push(item.id)

    if (item.static !== undefined && typeof item.static !== 'boolean') {
      throw new Error('VueGridLayout: ' + contextName + '[' + index + '].static must be a boolean!')
    }
  }
}
